package com.jiao.autoconfig.config;

import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.util.ClassUtils;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description
 * @Author Vincent.jiao
 * @Date 2022/5/16 13:46
 */
public final class CacheDataSourceBuilder<T extends DataSource> {

    private static final String[] DATA_SOURCE_TYPE_NAMES = new String[] { "com.zaxxer.hikari.HikariDataSource",
            "org.apache.tomcat.jdbc.pool.DataSource", "org.apache.commons.dbcp2.BasicDataSource" };

    private Class<? extends DataSource> type;

    private ClassLoader classLoader;

    private Map<String, String> properties = new HashMap<>();

    public static CacheDataSourceBuilder<?> create() {
        return new CacheDataSourceBuilder<>(null);
    }

    public static CacheDataSourceBuilder<?> create(ClassLoader classLoader) {
        return new CacheDataSourceBuilder<>(classLoader);
    }

    private CacheDataSourceBuilder(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @SuppressWarnings("unchecked")
    public T build() throws ClassNotFoundException {
        Class<? extends DataSource> type = getType();
        DataSource result = new CacheDataSource(type);
        maybeGetDriverClassName();
        bind(result);
        return (T) result;
    }

    private void maybeGetDriverClassName() {
        if (!this.properties.containsKey("driverClassName") && this.properties.containsKey("url")) {
            String url = this.properties.get("url");
            String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
            this.properties.put("driverClassName", driverClass);
        }
    }

    private void bind(DataSource result) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(this.properties);
        ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
        aliases.addAliases("driver-class-name", "driver-class");
        aliases.addAliases("url", "jdbc-url");
        aliases.addAliases("username", "user");
        Binder binder = new Binder(source.withAliases(aliases));
        binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
    }

    @SuppressWarnings("unchecked")
    public <D extends DataSource> CacheDataSourceBuilder<D> type(Class<D> type) {
        this.type = type;
        return (CacheDataSourceBuilder<D>) this;
    }

    public CacheDataSourceBuilder<T> url(String url) {
        this.properties.put("url", url);
        return this;
    }

    public CacheDataSourceBuilder<T> driverClassName(String driverClassName) {
        this.properties.put("driverClassName", driverClassName);
        return this;
    }

    public CacheDataSourceBuilder<T> username(String username) {
        this.properties.put("username", username);
        return this;
    }

    public CacheDataSourceBuilder<T> password(String password) {
        this.properties.put("password", password);
        return this;
    }

    public CacheDataSourceBuilder<T> type(String type) {
        this.properties.put("type", type);
        return this;
    }

    @SuppressWarnings("unchecked")
    public static Class<? extends DataSource> findType(ClassLoader classLoader) {
        for (String name : DATA_SOURCE_TYPE_NAMES) {
            try {
                return (Class<? extends DataSource>) ClassUtils.forName(name, classLoader);
            }
            catch (Exception ex) {
                // Swallow and continue
            }
        }
        return null;
    }

    private Class<? extends DataSource> getType() {
        Class<? extends DataSource> type = (this.type != null) ? this.type : findType(this.classLoader);
        if (type != null) {
            return type;
        }
        throw new IllegalStateException("No supported DataSource type found");
    }
}
